NSLog使用 与 LLDB命令

调试是程序开发的重要的功能组成部分,可以用来发现程序出存在的问题,快速定位及解决,调试也可以辅助更好的理解程序。事实上,调试最初就是为了解决问题而产生的,调试的英文为debug, 而bug就是程序中存在的问题,debug就是解决掉这些问题。

1.调试的功能:

在调试中,程序的每一步的执行均是可控的,可以通过单步执行,设置断点等,控制程序的运行节奏,并在每次暂停时,都可以查看当前有效变量的具体值。

2.对初学者的意义:

调试中可以很明确的看到程序的执行过程,以及每一步产生的变化,对于初学者,这样直观的体验自然要比读代码更容易理解,而看到现象之后,再回想理论也更容易。

3.对编程者的意义:

当程序的运行结果与期望不符时,可以通过调试,查看每步的具体执行及结果,因此可以定位出到底是哪个操作或语句与预期的不同,从而快速定位,再针对性分析代码,实现快速解决问题的目的。

NSLog 使用

在XCode做开发调试时往往需要打印一些调试信息做debug用

NSLog 性能问题

它的运行会占用时间和设备资源。当打印信息的地方多了之后在模拟器上跑可能不会有什么问题,因为模拟器用的是电脑的硬件,但是当应用跑在设备上时这些输出语句会在很大程度上影响应用的性能,而且输出的数据也可能会暴露出App里的保密数据,所以发布正式版时需要把这些输出全部屏蔽掉,针对这种问题可以写一些宏来控制这些调试信息的输出。

解决方案:

简单粗暴的解决方案:在APP release前,将所有的NSLog注释掉,简单有效,但副作用是:下次你要调试时,又得将NSLog一个个取消注释。

正确的解决方案:你以release模式编译的程序不会用NSLog输出,而你以debug模式编译的程序将执行NSLog的全部功能。 在release版本禁止输出NSLog内容。

如何实现?

  1. 在 xxx.pch (预编译文件) 中添加以下代码:
1
2
3
4
5
6
7
8
//用宏指令做一个判断,如果DEBUG为真,则编译#ifdef到#endif宏定义,否则编译器就不编译;
#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s", __func__)
#else
#define NSLog(...)
#define debugMethod()
#endif
  1. 设置DEBUG

在 “Target > Build Settings > Preprocessor Macros > Debug” 里有一个”DEBUG=1”。

设置为Debug模式下,点击Product –> Scheme –> Edit Scheme

设置run –> info –> Build Configuration成Debug时,就可以打印nslog了。

设置Release,发布app版本的时候就不会打印了,提高了性能。

如何自定义输入

在 xxx.pch 中添加以下代码:

1
2
3
4
5
#ifdef DEBUG
#define NSLog(format, ...) fprintf(stderr, "class:%s \nline: %d \nmethod:%s \nmessage:%s \n%s \n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__, __func__,[[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String], [@"----------------------------------------------" UTF8String]);
#else
#define NSLog(format, ...)
#endif

可以根据个人需要调整上面代码,主要就是这几个宏 ##__VA_ARGS , \FILE , \LINE 和\FUNCTION__。

1
2
3
4
5
1) __VA_ARGS__ 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错, 你可以试试。
2) __FILE__ 宏在预编译时会替换成当前的源文件名
3) __LINE__宏在预编译时会替换成当前的行号
4) __FUNCTION__宏在预编译时会替换成当前的函数名称
5) __func__ 打印当前函数或方法,c字符串

将Log日志重定向输出到文件

对于真机,日志没法保存,不好分析问题。所以有必要将日志保存到应用的Docunment目录下,并设置成共享文件,这样才能取出分析。

首先是日志输出,分为c的printf和标准的NSLog输出,printf会向标准输出(sedout)打印,而NSLog则是向标准出错(stderr),我们需要同时让他们都将日志打印到一个文件中。

1
2
3
//例子:
freopen("xx.log","a+",stdout);
freopen("xx.log","a+",stderr);

具体做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将NSlog打印信息保存到Document目录下的文件中
- (void)redirectNSlogToDocumentFolder
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [paths objectAtIndex:0];
NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:@"log.txt"];

// 先删除已经存在的文件
NSFileManager *defaultManager = [NSFileManager defaultManager];
[defaultManager removeItemAtPath:logFilePath error:nil];

// 将log输入到文件
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
}

此函数要在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中调用。

当连接Mac调试的时候把这些注释掉,否则log只会输入到文件中,而不能从xcode的监视器中看到。

最后配置共享文件夹:

在应用程序的Info.plist文件中添加UIFileSharingEnabled键(Application supports iTunes file sharing 键),并将键值设置为YES。将您希望共享的文件放在应用程序的 Documents目录。一旦设备插入到用户计算机,iTunes 9.1就会在选中设备的Apps标签中找到自己的应用,查看共享内容,找到 log.txt 文件。

断言NSAssert()的使用

NSAssert()只是一个宏,用于开发阶段调试程序中的Bug,通过为NSAssert()传递条件表达式来断定是否属于Bug,满足条件返回真值,程序继续运行,如果返回假值,则抛出异常,并切可以自定义异常描述。NSAssert()是这样定义的:

1
#define NSAssert(condition, desc)

condition是条件表达式,值为YES或NO;desc为异常描述,通常为NSString。当conditon为YES时程序继续运行,为NO时,则抛出带有desc描述的异常信息。NSAssert()可以出现在程序的任何一个位置。具体事例如下:

生成一个LotteryEntry对象时,传入的NSDate不能为nil,加入NSAssert()判断。对象初始化源码如下:

1
2
3
4
5
6
7
8
9
10
- (id)initWithEntryDate:(NSDate *)theDate {
self = [super init];
if (self) {
NSAssert(theDate != nil, @"Argument must be non-nil");
entryDate = theDate;
firstNumber = (int)random() % 100 + 1;
secondNumber = (int)random() % 100 + 1;
}
return self;
}

接下来则是生成对象时传入一个值为nil的NSDate,看断言是否运行。

LotteryEntry *nilEntry = [[LotteryEntry alloc] initWithEntryDate:nil];

断言效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
2013-01-17 20:49:12.486 lottery[3951:303] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Argument must be non-nil'

*** First throw call stack:
(
0 CoreFoundation 0x00007fff90c590a6 __exceptionPreprocess + 198
1 libobjc.A.dylib 0x00007fff8fd2a3f0 objc_exception_throw + 43
2 CoreFoundation 0x00007fff90c58ee8 +[NSException raise:format:arguments:] + 104
3 Foundation 0x00007fff88dae6a2 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 189
4 lottery 0x0000000100001929 -[LotteryEntry initWithEntryDate:] + 249
5 lottery 0x0000000100001794 main + 932
6 libdyld.dylib 0x00007fff8d83f7e1 start + 0
)
libc++abi.dylib: terminate called throwing an exception

移除NSAssert比较简单,我们需要在target中选择build settings, 找到 preprocessor macros(预处理宏)项目,配置它的release为 NS_BLOCK_ASSERTIONS。

具体操作步骤为: 双击release的空白处,此时会弹出对话框,点击对话框中的+添加NS_BLOCK_ASSERTIONS。

Xcode 的LLDB 调试

先说一下Xcode怎样添加断点,以及调试区域在哪里,话不多说,请看图:

当代码走到断点处,会进入调试模式,在Xcode右下方的调试区域,有 (lldb) 显示。

常用 lldb 命令

  • po 或 p 命令

调试器中最常用到的命令是 p (用于输出基本类型)或者 po (用于输出 Objective-C 对象),示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 调试命令
po username

// 运行结果
admin

// 调试命令
p username

// 运行结果
(__NSCFConstantString *) $1 = 0x000000010c0f32a0 @"admin"

/**
1. 每次查询的结果会保存在一些持续变量中($[0-9]+),这样你可以在后面的查询中直接使用这些值。比如现在我接下来要重新取回 $1 的值:p $1
运行结果:(__NSCFConstantString *) $1 = 0x000000010c0f32a0 @"admin"

2. 0x000000010c0f32a0 是username 的地址
**/

po 或 p 命令远没你想的那么简单,他还有执行代码的功能,比如:

1
2
3
4
// 执行命令
po [self.view setBackgroundColor:[UIColor redColor]]

然后你就发现当前视图的背景变成红色了。
  • expression 或 expr 命令

常用于在调试过程中修改变量的值,而不用重新启动程序。

1
2
3
4
5
6
7
8
9
10
11
// 执行命令
expression username = @"hello"

// 执行结果
(NSTaggedPointerString *) $0 = 0xa00006f6c6c65685 @"hello"

// 执行命令
po username

// 执行结果
hello

除了上面的用途,还可以新声明一个变量,比如:

1
2
3
4
5
6
7
8
// 执行命令
expression NSString * $a = @"xxxx" // $符号不要丢了

// 执行命令
po $a

// 执行结果
xxxx
  • call 命令

call 命令和 po 命令的功能相似,有的地方说“在不需要显示输出,或是方法无返回值时使用call”,但是使用 po 命令系统会向编代码一样提示你方法名或变量名,但是 call 命令不会啊,没想出有什么理由放弃 po 而使用 call 。

  • bt 命令

bt 命令用来打印主线程的堆栈信息,bt all 可以打印所有线程的堆栈信息。

  • image 命令

image 命令可用于寻址,有多个组合命令。比较实用的用法是用于寻找栈地址对应的代码位置。

比如以下代码:

1
2
NSArray *array = @[@"1",@"2"];
NSLog(@"%@",array[3]);

这段代码会报数组越界的错误,错误信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x0000000107d49d85 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001077bddeb objc_exception_throw + 48
2 CoreFoundation 0x0000000107c32934 -[__NSArrayI objectAtIndex:] + 164
3 demo 0x00000001072b51f6 -[ViewController viewDidLoad] + 390
4 UIKit 0x000000010829a984 -[UIViewController loadViewIfRequired] + 1198
5 UIKit 0x000000010829acd3 -[UIViewController view] + 27
6 UIKit 0x0000000108170fb4 -[UIWindow addRootViewControllerViewIfPossible] + 61
7 UIKit 0x000000010817169d -[UIWindow _setHidden:forced:] + 282
8 UIKit 0x0000000108183180 -[UIWindow makeKeyAndVisible] + 42
9 UIKit 0x00000001080f7ed9 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131
10 UIKit 0x00000001080fe568 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1769
11 UIKit 0x00000001080fb714 -[UIApplication workspaceDidEndTransaction:] + 188
12 FrontBoardServices 0x000000010ab688c8 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
13 FrontBoardServices 0x000000010ab68741 -[FBSSerialQueue _performNext] + 178
14 FrontBoardServices 0x000000010ab68aca -[FBSSerialQueue _performNextFromRunLoopSource] + 45
15 CoreFoundation 0x0000000107c6f301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
16 CoreFoundation 0x0000000107c6522c __CFRunLoopDoSources0 + 556
17 CoreFoundation 0x0000000107c646e3 __CFRunLoopRun + 867
18 CoreFoundation 0x0000000107c640f8 CFRunLoopRunSpecific + 488
19 UIKit 0x00000001080faf21 -[UIApplication _run] + 402
20 UIKit 0x00000001080fff09 UIApplicationMain + 171
21 demo 0x00000001072b5bdf main + 111
22 libdyld.dylib 0x000000010a52392d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

可能出错的地址是0x00000001072b51f6(可以根据执行文件名判断,或者最小的栈地址)。为了进一步精确定位,我们可以输入以下的命令:

1
2
3
4
5
6
7
8
// 执行命令
image lookup --address 0x00000001072b51f6

// 运行结果
Address: demo[0x00000001000011f6] (demo.__TEXT.__text + 390)
Summary: demo`-[ViewController viewDidLoad] + 390 at ViewController.m:31

告诉我们出错位置是 ViewController.m 类的 31 行。
  • 简称和别名

有些调试命令比较长,使用起来比较麻烦,我们可以自定义别名。比如给image lookup –address 添加了一个 ila 的别名。

1
2
3
4
5
6
7
8
9
// 执行命令
command alias ila image lookup --address %1

// 执行命令
ila 0x00000001072b51f6

// 运行结果
Address: demo[0x00000001000011f6] (demo.__TEXT.__text + 390)
Summary: demo`-[ViewController viewDidLoad] + 390 at ViewController.m:31

等等,还没有结束,还有一个非常重要的命令 help,如果你对expr 命令不了解,可以使用help expr 来查看更多关于expr 命令的信息。

调试技巧

请看图:

双击断点,选择编辑选项,开始编辑断点。

1
2
3
4
5
Condition:此处是判断断点的执行条件,当(BOOL)[username isEqualToString:@"admin"]为 YES 时,执行断点。

Action:此处是执行的调试命令,比如 expr username = @"hello" 等改变变量值的命令,或者格式化输出 po [NSString stringWithFormat:@"username:%@ \npassword:%@",username,password],当然还可以执行其他命令。

Options:此处勾选,断点处不会进入调试模式。

总结

调试方法的灵活使用,可以减轻开发负担,同时也能更加准确的找到问题所在。这里只是平时常用的几个命令,想要更多的命令,还需要自己取学习。

参考网站 The LLDB Debugger

如果觉得写的不错,那就打赏一下吧!